<template>
  <div>
    <header
      ref="header"
      class="layout__col-2-1 header"
      :class="{ 'header--overflow': headerOverflow }"
    >
      <div v-show="tableLoading" class="header__loading"></div>
      <ul v-if="!tableLoading" class="header__filter">
        <li class="header__filter-item header__filter-item--grids">
          <a
            ref="viewsSelectToggle"
            class="header__filter-link"
            @click="
              $refs.viewsContext.toggle(
                $refs.viewsSelectToggle,
                'bottom',
                'left',
                4
              )
            "
          >
            <template v-if="hasSelectedView">
              <i
                class="header__filter-icon header-filter-icon--view fas"
                :class="'fa-' + view._.type.iconClass"
              ></i>
              <span class="header__filter-name header__filter-name--forced">{{
                view.name
              }}</span>
            </template>
            <span v-else>
              <i
                class="header__filter-icon header-filter-icon-no-choice fas fa-caret-square-down"
              ></i>
              Choose view
            </span>
          </a>
          <ViewsContext
            ref="viewsContext"
            :table="table"
            :views="views"
            :read-only="readOnly"
            :header-overflow="headerOverflow"
            @selected-view="$emit('selected-view', $event)"
          ></ViewsContext>
        </li>
        <li
          v-if="hasSelectedView && view._.type.canFilter"
          class="header__filter-item"
        >
          <ViewFilter
            :view="view"
            :fields="fields"
            :primary="primary"
            :read-only="readOnly"
            @changed="refresh()"
          ></ViewFilter>
        </li>
        <li
          v-if="hasSelectedView && view._.type.canSort"
          class="header__filter-item"
        >
          <ViewSort
            :view="view"
            :fields="fields"
            :primary="primary"
            :read-only="readOnly"
            @changed="refresh()"
          ></ViewSort>
        </li>
      </ul>
      <component
        :is="getViewHeaderComponent(view)"
        v-if="!tableLoading && hasSelectedView"
        :database="database"
        :table="table"
        :view="view"
        :fields="fields"
        :primary="primary"
        :read-only="readOnly"
        :store-prefix="storePrefix"
        @refresh="refresh"
      />
    </header>
    <div class="layout__col-2-2 content">
      <component
        :is="getViewComponent(view)"
        v-if="hasSelectedView && !tableLoading"
        ref="view"
        :database="database"
        :table="table"
        :view="view"
        :fields="fields"
        :primary="primary"
        :read-only="readOnly"
        :store-prefix="storePrefix"
        @refresh="refresh"
      />
      <div v-if="viewLoading" class="loading-overlay"></div>
    </div>
  </div>
</template>

<script>
import ResizeObserver from 'resize-observer-polyfill'

import { RefreshCancelledError } from '@baserow/modules/core/errors'
import { notifyIf } from '@baserow/modules/core/utils/error'
import ViewsContext from '@baserow/modules/database/components/view/ViewsContext'
import ViewFilter from '@baserow/modules/database/components/view/ViewFilter'
import ViewSort from '@baserow/modules/database/components/view/ViewSort'
import ViewSearch from '@baserow/modules/database/components/view/ViewSearch'

/**
 * This page component is the skeleton for a table. Depending on the selected view it
 * will load the correct components into the header and body.
 */
export default {
  components: {
    ViewsContext,
    ViewFilter,
    ViewSort,
    ViewSearch,
  },
  /**
   * Because there is no hook that is called before the route changes, we need the
   * tableLoading middleware to change the table loading state. This change will get
   * rendered right away. This allows us to have a custom loading animation when
   * switching views.
   */
  middleware: ['tableLoading'],
  props: {
    database: {
      type: Object,
      required: true,
    },
    table: {
      type: Object,
      required: true,
    },
    fields: {
      type: Array,
      required: true,
    },
    primary: {
      type: Object,
      required: true,
    },
    views: {
      type: Array,
      required: true,
    },
    view: {
      type: Object,
      required: true,
      validator: (prop) => typeof prop === 'object' || prop === undefined,
    },
    tableLoading: {
      type: Boolean,
      required: true,
    },
    readOnly: {
      type: Boolean,
      required: false,
      default: false,
    },
    storePrefix: {
      type: String,
      required: false,
      default: '',
    },
  },
  data() {
    return {
      // Shows a small spinning loading animation when the view is being refreshed.
      viewLoading: false,
      // Indicates if the elements within the header are overflowing. In case of true,
      // we can hide certain values to make sure that it fits within the header.
      headerOverflow: false,
    }
  },
  computed: {
    /**
     * Indicates if there is a selected view by checking if the view object has been
     * populated.
     */
    hasSelectedView() {
      return (
        this.view !== undefined &&
        Object.prototype.hasOwnProperty.call(this.view, '_')
      )
    },
  },
  watch: {
    tableLoading(value) {
      if (!value) {
        this.$nextTick(() => {
          this.checkHeaderOverflow()
        })
      }
    },
  },
  beforeMount() {
    this.$bus.$on('table-refresh', this.refresh)
  },
  mounted() {
    this.$el.resizeObserver = new ResizeObserver(this.checkHeaderOverflow)
    this.$el.resizeObserver.observe(this.$el)
  },
  beforeDestroy() {
    this.$bus.$off('table-refresh', this.refresh)
    this.$el.resizeObserver.unobserve(this.$el)
  },
  methods: {
    getViewComponent(view) {
      const type = this.$registry.get('view', view.type)
      return type.getComponent()
    },
    getViewHeaderComponent(view) {
      const type = this.$registry.get('view', view.type)
      return type.getHeaderComponent()
    },
    /**
     * When the window resizes, we want to check if the content of the header is
     * overflowing. If that is the case, we want to make some space by removing some
     * content. We do this by copying the header content into a new HTMLElement and
     * check if the elements still fit within the header. We copy the html because we
     * want to measure the header in the full width state.
     */
    checkHeaderOverflow() {
      const header = this.$refs.header
      const width = header.getBoundingClientRect().width
      const wrapper = document.createElement('div')
      wrapper.innerHTML = header.outerHTML
      const el = wrapper.childNodes[0]
      el.style = `position: absolute; left: 0; top: 0; width: ${width}px; overflow: auto;`
      el.classList.remove('header--overflow')
      document.body.appendChild(el)
      this.headerOverflow =
        el.clientWidth < el.scrollWidth || el.clientHeight < el.scrollHeight
      document.body.removeChild(el)
    },
    /**
     * Refreshes the whole view. All data will be reloaded and it will visually look
     * the same as seeing the view for the first time.
     */
    async refresh(event) {
      // If could be that the refresh event is for a specific table and in table case
      // we check if the refresh event is related to this table and stop if that is not
      // the case.
      if (
        typeof event === 'object' &&
        Object.prototype.hasOwnProperty.call(event, 'tableId') &&
        event.tableId !== this.table.id
      ) {
        return
      }

      this.viewLoading = true
      const type = this.$registry.get('view', this.view.type)
      try {
        await type.refresh(
          { store: this.$store },
          this.view,
          this.fields,
          this.primary,
          this.storePrefix
        )
      } catch (error) {
        if (error instanceof RefreshCancelledError) {
          // Multiple refresh calls have been made and the view has indicated that
          // this particular one should be cancelled. However we do not want to
          // set viewLoading back to false as the other non cancelled call/s might
          // still be loading.
          return
        } else {
          notifyIf(error)
        }
      }
      if (
        Object.prototype.hasOwnProperty.call(this.$refs, 'view') &&
        Object.prototype.hasOwnProperty.call(this.$refs.view, 'refresh')
      ) {
        await this.$refs.view.refresh()
      }
      // It might be possible that the event has a callback that needs to be called
      // after the rows are refreshed. This is for example the case when a field has
      // changed. In that case we want to update the field in the store after the rows
      // have been refreshed to prevent incompatible values in field types.
      if (event && Object.prototype.hasOwnProperty.call(event, 'callback')) {
        await event.callback()
      }
      this.$nextTick(() => {
        this.viewLoading = false
      })
    },
  },
}
</script>
